
#include "SolidworksWriteProcess.hpp"
#include "datakit.h"
#include <iostream>
#include "tess/tess.h"
#include "common/logger.h"
#include "swwrite/solidworksw.hpp"
#include "./utils/FileUtil.h"
#include "./utils/StringUtil.h"
#ifndef CXX_LESS_17
#include <filesystem>
#else
    #include <codecvt>
    #include <locale>
#endif

namespace AMCAX::Exchange
{
    SolidworksWriteProcess::SolidworksWriteProcess(Dtk_string inputFile, std::unique_ptr<WriterBase> writer)
        : WriteProcessBase(inputFile, std::move(writer))
    {
    }

    Dtk_ErrorStatus SolidworksWriteProcess::WriteDocument(Dtk_MainDocPtr inDocument, const std::string& outputFile)
    {
        if (inDocument.IsNULL())
            return dtkErrorNullPointer;

        // First we get the root component in the document
        Dtk_ComponentPtr rootComponent = inDocument->RootComponent();
        if (rootComponent.IsNULL())
        {
            return dtkErrorNullPointer;
        }

        // Fix the file type if needed.
        std::string outputFilePath(outputFile);
        auto extension = StringUtil::ToLowerCase(FileUtil::GetExtension(outputFile));
        auto isAssembly = rootComponent->IsAssembly();
        if(isAssembly)
        {
            if(extension != ".sldasm")
            {
                outputFilePath += ".sldasm";
            }
        }
        else
        {
            if(extension != ".sldprt")
            {
                outputFilePath += ".sldprt";
            }
        }

        // if no Error we write the Component
        if (m_fileWriter)
        {
            auto err = m_fileWriter->WriteInit(m_inputFile, outputFilePath.c_str());
            if (err != dtkNoError)
            {
                LOG_ERROR("Write Init with error {}", (int)err);
                return err;
            }
        }

        if (!isAssembly)
        {
            auto status = solidworksw::OpenPart(outputFilePath.c_str());
            if (status != dtkNoError)
            {
                std::cout << "open output part error : " << dtkTypeError(status).c_str() << std::endl;
                return status;
            }
        }

        // Go Through the Root Component and children
        const Dtk_transfo &inMatrix = Dtk_transfo();
        WriteComponent(rootComponent, inMatrix, false);

        if (!isAssembly)
        {
            auto status = solidworksw::ClosePart();
            if (status != dtkNoError)
            {
                std::cout << "close output part error : " << dtkTypeError(status).c_str() << std::endl;
                return status;
            }
        }        

        // Build assembly part relationships.
        BuildPartAssemblyRelationships(outputFilePath);

        if (m_fileWriter)
        {
            auto err = m_fileWriter->WriteEnd();
            if (err != dtkNoError)
            {
                LOG_ERROR("Write End with error {}", (int)err);
                return err;
            }
        }
        return dtkNoError;
    }

    Dtk_ErrorStatus SolidworksWriteProcess::WriteInstance(Dtk_ComponentPtr inComponent)
    {

        // Instance represent a prototype with a matrix placement
        Dtk_string componentName;
        componentName = inComponent->Name(); // Component name
        Dtk_ComponentPtr prototype = inComponent->GetChild(0);
        Dtk_transfo matrix = inComponent->TransformationMatrix();
        Dtk_ID childID = inComponent->GetChild(0)->GetID();

        // you have to write matrix and instance the prototype
        WriteComponent(prototype, Dtk_transfo(), ComponentProcessed.find(childID) < 0);
        if (ComponentProcessed.find(childID) < 0)
        {
            ComponentProcessed.push_back(childID);
        }

        return dtkNoError;
    }

    Dtk_ErrorStatus SolidworksWriteProcess::WriteComponent(Dtk_ComponentPtr inComponent, const Dtk_transfo& inMatrix, bool bCreateFile)
    {
        Dtk_Size_t i;

        // Datakit component are given in MM, if original model had other unit you can get it with GetConceptionUnitScale()
        //  unitFactor = 25.4 for inch
        double unitFactor;
        Dtk_ErrorStatus err;
        err = inComponent->GetConceptionUnitScale(unitFactor);

        // GetAttributes
        Dtk_InfoPtr attributes = inComponent->GetInfos();
        if (attributes.IsNotNULL())
        {
            int compoActivationStatus, compoBlankedStatus;
            Dtk_RGB compoColor;
            compoActivationStatus = attributes->GetActivationFlag(); // if compoActivationStatus == 0 Component and his children aren't visible
            compoBlankedStatus = attributes->GetBlankedStatus();     // if compoBlankedStatus == 1 Component and his children aren't visible
            compoColor = attributes->GetColor();                     // if compoBlankedStatus == 1 Component and his children are using this color
        }

        if (m_fileWriter && bCreateFile)
        {
            m_fileWriter->InitComponent(inComponent);
        }
        // You have 4 types for Component
        Dtk_Component::ComponentTypeEnum type = inComponent->ComponentType();

        switch (type)
        {
            // Instance represent a prototype with a matrix placement
        case Dtk_Component::InstanceComponentType:
        {
            WriteInstance(inComponent);
            break;
        }
        // Prototype (you have to check if you ever read and write it to don't waste time)
        // you can use the method inComponent->GetID() to get Unique ID for Component
        case Dtk_Component::PrototypeComponentType:
        {
            Dtk_ID outId;
            WritePrototype(inComponent, inMatrix, outId);
            break;
        }
        // Catalog Component represent a choice of several possible configuration
        //(like scene in catiav5, workspace in catiav4, configuration in solidworks)
        // Default is the first child
        case Dtk_Component::CatalogComponentType:
        {
            Dtk_string name = inComponent->Name(); // Component name

            Dtk_Size_t numComp = inComponent->GetNumChildren();
            if (numComp > 0)
            {
                Dtk_Int32 defaultindex = inComponent->GetDefaultChildInCatalog(); // Get Default child to use
                Dtk_ComponentPtr defaultchoice = inComponent->GetChild(defaultindex);
                if (defaultchoice.IsNotNULL())
                {
                    WriteComponent(defaultchoice, inMatrix, true);
                }
            }
            // if you don't want to use default you have to scan all children and choose the one you want to convert (see their name)
            break;
        }
        // Component containing only children
        case Dtk_Component::VirtualComponentType:
        {
            Dtk_string name;
            Dtk_Size_t NumChildren;

            name = inComponent->Name(); // Component name

            NumChildren = inComponent->GetNumChildren();
            for (i = 0; i < NumChildren; i++)
            {
                Dtk_ComponentPtr child = inComponent->GetChild(i);
                WriteComponent(child, inMatrix);
            }
            break;
        }
        }

        if (m_fileWriter && bCreateFile)
        {
            auto name = inComponent->Name(); // Component name
            if(inComponent->IsAssembly())
            {
                m_asseblyIdAndNameMap[inComponent->GetID()] = inComponent->Name();
            }            
            m_fileWriter->EndComponent(inComponent);
        }
        return dtkNoError;
    }

    Dtk_ErrorStatus SolidworksWriteProcess::WritePrototype(Dtk_ComponentPtr inComponent, const Dtk_transfo& inMatrix, Dtk_ID &outPrototypeId)
    {
        Dtk_ErrorStatus err = dtkNoError;
        Dtk_Size_t i;
        Dtk_ID componentID = inComponent->GetID();
        Dtk_Int32 componentIndex;
        componentIndex = ComponentProcessed.find(componentID);

        if (componentIndex == -1)
        {
            Dtk_API *inAPI = Dtk_API::GetAPI();
            // it can also contain some instances
            Dtk_Size_t numChildren = inComponent->GetNumChildren();
            if(numChildren > 0)
            {
                m_asseblyIdAndNameMap[componentID] = inComponent->Name();
            }
            for (i = 0; i < numChildren; i++)
            {
                Dtk_ComponentPtr child = inComponent->GetChild(i);
                Dtk_ID childComponentID = child->GetID();
                m_assemblyPartsInfoMap[componentID].push_back(InstanceInfo(child->Name(), child->TransformationMatrix(), child->IsAssembly(), componentID));
                WriteComponent(child, inMatrix, false);
            }
            Dtk_NodePtr rootNode;

            // Get the Construction tree for this prototype
            if (inComponent->ComponentAvailability() == Dtk_Component::ComponentMissing)
                err = dtkErrorFileNotExist;
            else
                err = inAPI->ReadComponent(inComponent, rootNode);

            std::cout << "Read Component " << inComponent->Name().c_str() << std::endl;

            // a rootNode=  NULL with err == dtkNoError means that component is empty
            if (err == dtkNoError && rootNode.IsNotNULL())
            {
                // Set matrix to apply to all node if you flat assemblies
                m_currentMatrix = inMatrix;

                Dtk_tab<Dtk_ID> outIds;
                // Go Through the Root Node and children
                WriteNode(rootNode, outIds);

                if (m_fileWriter)
                {
                    m_fileWriter->WriteGlobalDataSet(inComponent->GetGlobalDataSet());
                    auto isAssembly = inComponent->IsAssembly();
                    Dtk_Size_t numMetaData;
                    numMetaData = inComponent->GetNumMetaData();
                    if (numMetaData)
                    {
                        for (i = 0; i < numMetaData; i++)
                        {
                            // For assembly component, we need to store the metadata in the map for later adding because wedidn't
                            // open the assembly yet.
                            // For part component, we can write it directly.
                            if(isAssembly)
                            {
                                m_assemblyMetaDataMap[componentID].push_back(inComponent->GetMetaData(i));
                            }
                            else
                            {
                                m_fileWriter->WriteMetaData(inComponent->GetMetaData(i));
                            }
                        }
                    }
                }
            }

            // We close the opened Component and free his construction tree
            inAPI->EndComponent(inComponent);
        }
        return err;
    }

    Dtk_ErrorStatus SolidworksWriteProcess::BuildPartAssemblyRelationships(const std::string& outputFile)
    {
        if (!m_fileWriter || m_asseblyIdAndNameMap.size() < 1)
            return dtkNoError;

        Dtk_ErrorStatus status = dtkNoError;
        #ifndef CXX_LESS_17
        std::filesystem::path outputFilePath = outputFile;
        auto parentDir = outputFilePath.parent_path();
        #else
        std::string outputFilePath = outputFile;
        auto parentDir = FileUtil::GetParentPath(outputFilePath) ;
        #endif

        // Rename the first file name to user specified assembly file name, as this is the output assembly file.
        auto firstAssembly = m_asseblyIdAndNameMap.begin();
        #ifndef CXX_LESS_17
        firstAssembly->second = outputFilePath.stem().c_str();
        #else
        firstAssembly->second = FileUtil::GetFilenameWithoutExtension(outputFilePath).c_str();
        #endif

        // loop for all the assemblies with reverse order
        for(auto iter = m_asseblyIdAndNameMap.rbegin(); iter != m_asseblyIdAndNameMap.rend(); ++iter)
        {
            auto key = iter->first;
            auto value = iter->second;
            Dtk_string fileName = value + L".sldasm";
            #ifndef CXX_LESS_17
            std::filesystem::path file = WriterBase::GetWStringFromDtkString(fileName);
            outputFilePath = (parentDir.lexically_normal() / file.lexically_normal());
            // Avoid duplicate assembly file names.(need to revisit.)
            if (std::filesystem::exists(outputFilePath))
            {
                continue;
            }
            status = solidworksw::OpenAssembly(outputFilePath.generic_wstring().c_str());
            #else
            std::wstring file = WriterBase::GetWStringFromDtkString(fileName);
            std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
            std::string temp = conv.to_bytes(file);
            outputFilePath = parentDir  + "/" + temp ;
            std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converteToWide;
            std::wstring wideStr = converteToWide.from_bytes(outputFilePath);
            status = solidworksw::OpenAssembly(wideStr.c_str());
            #endif

            auto it = m_assemblyPartsInfoMap.find(key);
            if (it != m_assemblyPartsInfoMap.end())
            {
                auto partsInfo = m_assemblyPartsInfoMap[key];
                for (auto partInfo : partsInfo)
                {
                    Dtk_string childFileName = partInfo.PrototypeName;
                    childFileName += partInfo.IsAssembly ? L".sldasm" : L".sldprt";
                    #ifndef CXX_LESS_17
                    std::filesystem::path childFile = WriterBase::GetWStringFromDtkString(childFileName);
                    auto childFilePath = (parentDir.lexically_normal() / childFile.lexically_normal());
                    solidworksw::Instance instance(childFilePath.generic_wstring().c_str(), partInfo.PrototypeName, partInfo.Transformation);
                    #else
                    auto childFile = WriterBase::GetWStringFromDtkString(childFileName);
                    temp = conv.to_bytes(childFile);
                    auto childFilePath = parentDir + "/ " +  temp;
                    wideStr = converteToWide.from_bytes(childFilePath);
                    solidworksw::Instance instance(wideStr.c_str(), partInfo.PrototypeName, partInfo.Transformation);
                    #endif

                    
                    status = solidworksw::AddInstanceComponent(instance);
                }

                // TODO: write meta data for the assembly returns the error dtkErrorNotProcessed by now.
                // Need to check if the usage is correct or not.
                if(m_fileWriter)
                {
                    auto metaDataIt = m_assemblyMetaDataMap.find(key);
                    if (metaDataIt != m_assemblyMetaDataMap.end())
                    {
                        auto metaData = metaDataIt->second;
                        for (auto& data : metaData)
                        {
                            m_fileWriter->WriteMetaData(data);
                        }
                    }
                }

            }
            status = solidworksw::CloseAssembly();
        }

        return status;
    }
}
